 /*******************************************************************************
  * Copyright (c) 2005 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM - Initial API and implementation
  *******************************************************************************/
 package org.eclipse.osgi.util;

 import java.io.IOException ;
 import java.io.InputStream ;
 import java.lang.reflect.Field ;
 import java.lang.reflect.Modifier ;
 import java.security.AccessController ;
 import java.security.PrivilegedAction ;
 import java.util.*;
 import org.eclipse.osgi.framework.debug.Debug;
 import org.eclipse.osgi.framework.log.FrameworkLog;
 import org.eclipse.osgi.framework.log.FrameworkLogEntry;

 /**
  * Common superclass for all message bundle classes. Provides convenience
  * methods for manipulating messages.
  * <p>
  * The <code>#bind</code> methods perform string substitution and should be considered a
  * convenience and <em>not</em> a full substitute replacement for <code>MessageFormat#format</code>
  * method calls.
  * </p>
  * <p>
  * Text appearing within curly braces in the given message, will be interpreted
  * as a numeric index to the corresponding substitution object in the given array. Calling
  * the <code>#bind</code> methods with text that does not map to an integer will result in an
  * {@link IllegalArgumentException}.
  * </p>
  * <p>
  * Text appearing within single quotes is treated as a literal. A single quote is escaped by
  * a preceeding single quote.
  * </p>
  * <p>
  * Clients who wish to use the full substitution power of the <code>MessageFormat</code> class should
  * call that class directly and not use these <code>#bind</code> methods.
  * </p>
  * <p>
  * Clients may subclass this type.
  * </p>
  *
  * @since 3.1
  */
 public abstract class NLS {

     private static final Object [] EMPTY_ARGS = new Object [0];
     private static final String EXTENSION = ".properties"; //$NON-NLS-1$
 private static String [] nlSuffixes;
     /*
      * NOTE do not change the name of this field; it is set by the Framework using reflection
      */
     private static FrameworkLog frameworkLog;

     static final int SEVERITY_ERROR = 0x04;
     static final int SEVERITY_WARNING = 0x02;
     /*
      * This object is assigned to the value of a field map to indicate
      * that a translated message has already been assigned to that field.
      */
     static final Object ASSIGNED = new Object ();

     /**
      * Creates a new NLS instance.
      */
     protected NLS() {
         super();
     }

     /**
      * Bind the given message's substitution locations with the given string value.
      *
      * @param message the message to be manipulated
      * @param binding the object to be inserted into the message
      * @return the manipulated String
      */
     public static String bind(String message, Object binding) {
         return internalBind(message, null, String.valueOf(binding), null);
     }

     /**
      * Bind the given message's substitution locations with the given string values.
      *
      * @param message the message to be manipulated
      * @param binding1 An object to be inserted into the message
      * @param binding2 A second object to be inserted into the message
      * @return the manipulated String
      */
     public static String bind(String message, Object binding1, Object binding2) {
         return internalBind(message, null, String.valueOf(binding1), String.valueOf(binding2));
     }

     /**
      * Bind the given message's substitution locations with the given string values.
      *
      * @param message the message to be manipulated
      * @param bindings An array of objects to be inserted into the message
      * @return the manipulated String
      */
     public static String bind(String message, Object [] bindings) {
         return internalBind(message, bindings, null, null);
     }

     /**
      * Initialize the given class with the values from the specified message bundle.
      *
      * @param bundleName fully qualified path of the class name
      * @param clazz the class where the constants will exist
      */
     public static void initializeMessages(final String bundleName, final Class clazz) {
         if (System.getSecurityManager() == null) {
             load(bundleName, clazz);
             return;
         }
         AccessController.doPrivileged(new PrivilegedAction () {
             public Object run() {
                 load(bundleName, clazz);
                 return null;
             }
         });
     }

     /*
      * Perform the string substitution on the given message with the specified args.
      * See the class comment for exact details.
      */
     private static String internalBind(String message, Object [] args, String argZero, String argOne) {
         if (message == null)
             return "No message available."; //$NON-NLS-1$
 if (args == null || args.length == 0)
             args = EMPTY_ARGS;

         int length = message.length();
         //estimate correct size of string buffer to avoid growth
 int bufLen = length + (args.length * 5);
         if (argZero != null)
             bufLen += argZero.length() - 3;
         if (argOne != null)
             bufLen += argOne.length() - 3;
         StringBuffer buffer = new StringBuffer (bufLen);
         for (int i = 0; i < length; i++) {
             char c = message.charAt(i);
             switch (c) {
                 case '{' :
                     int index = message.indexOf('}', i);
                     // if we don't have a matching closing brace then...
 if (index == -1) {
                         buffer.append(c);
                         break;
                     }
                     i++;
                     if (i >= length) {
                         buffer.append(c);
                         break;
                     }
                     // look for a substitution
 int number = -1;
                     try {
                         number = Integer.parseInt(message.substring(i, index));
                     } catch (NumberFormatException e) {
                         throw new IllegalArgumentException ();
                     }
                     if (number == 0 && argZero != null)
                         buffer.append(argZero);
                     else if (number == 1 && argOne != null)
                         buffer.append(argOne);
                     else {
                         if (number >= args.length || number < 0) {
                             buffer.append("<missing argument>"); //$NON-NLS-1$
 i = index;
                             break;
                         }
                         buffer.append(args[number]);
                     }
                     i = index;
                     break;
                 case '\'' :
                     // if a single quote is the last char on the line then skip it
 int nextIndex = i + 1;
                     if (nextIndex >= length) {
                         buffer.append(c);
                         break;
                     }
                     char next = message.charAt(nextIndex);
                     // if the next char is another single quote then write out one
 if (next == '\'') {
                         i++;
                         buffer.append(c);
                         break;
                     }
                     // otherwise we want to read until we get to the next single quote
 index = message.indexOf('\'', nextIndex);
                     // if there are no more in the string, then skip it
 if (index == -1) {
                         buffer.append(c);
                         break;
                     }
                     // otherwise write out the chars inside the quotes
 buffer.append(message.substring(nextIndex, index));
                     i = index;
                     break;
                 default :
                     buffer.append(c);
             }
         }
         return buffer.toString();
     }

     /*
      * Build an array of property files to search. The returned array contains
      * the property fields in order from most specific to most generic.
      * So, in the FR_fr locale, it will return file_fr_FR.properties, then
      * file_fr.properties, and finally file.properties.
      */
     private static String [] buildVariants(String root) {
         if (nlSuffixes == null) {
             //build list of suffixes for loading resource bundles
 String nl = Locale.getDefault().toString();
             ArrayList result = new ArrayList(4);
             int lastSeparator;
             while (true) {
                 result.add('_' + nl + EXTENSION);
                 lastSeparator = nl.lastIndexOf('_');
                 if (lastSeparator == -1)
                     break;
                 nl = nl.substring(0, lastSeparator);
             }
             //add the empty suffix last (most general)
 result.add(EXTENSION);
             nlSuffixes = (String []) result.toArray(new String [result.size()]);
         }
         root = root.replace('.', '/');
         String [] variants = new String [nlSuffixes.length];
         for (int i = 0; i < variants.length; i++)
             variants[i] = root + nlSuffixes[i];
         return variants;
     }

     private static void computeMissingMessages(String bundleName, Class clazz, Map fieldMap, Field [] fieldArray, boolean isAccessible) {
         // iterate over the fields in the class to make sure that there aren't any empty ones
 final int MOD_EXPECTED = Modifier.PUBLIC | Modifier.STATIC;
         final int MOD_MASK = MOD_EXPECTED | Modifier.FINAL;
         final int numFields = fieldArray.length;
         for (int i = 0; i < numFields; i++) {
             Field field = fieldArray[i];
             if ((field.getModifiers() & MOD_MASK) != MOD_EXPECTED)
                 continue;
             //if the field has a a value assigned, there is nothing to do
 if (fieldMap.get(field.getName()) == ASSIGNED)
                 continue;
             try {
                 // Set a value for this empty field. We should never get an exception here because
 // we know we have a public static non-final field. If we do get an exception, silently
 // log it and continue. This means that the field will (most likely) be un-initialized and
 // will fail later in the code and if so then we will see both the NPE and this error.
 String value = "NLS missing message: " + field.getName() + " in: " + bundleName; //$NON-NLS-1$ //$NON-NLS-2$
 if (Debug.DEBUG_MESSAGE_BUNDLES)
                     System.out.println(value);
                 log(SEVERITY_WARNING, value, null);
                 if (!isAccessible)
                     field.setAccessible(true);
                 field.set(null, value);
             } catch (Exception e) {
                 log(SEVERITY_ERROR, "Error setting the missing message value for: " + field.getName(), e); //$NON-NLS-1$
 }
         }
     }

     /*
      * Load the given resource bundle using the specified class loader.
      */
     static void load(final String bundleName, Class clazz) {
         long start = System.currentTimeMillis();
         final Field [] fieldArray = clazz.getDeclaredFields();
         ClassLoader loader = clazz.getClassLoader();

         boolean isAccessible = (clazz.getModifiers() & Modifier.PUBLIC) != 0;

         //build a map of field names to Field objects
 final int len = fieldArray.length;
         Map fields = new HashMap(len * 2);
         for (int i = 0; i < len; i++)
             fields.put(fieldArray[i].getName(), fieldArray[i]);

         // search the variants from most specific to most general, since
 // the MessagesProperties.put method will mark assigned fields
 // to prevent them from being assigned twice
 final String [] variants = buildVariants(bundleName);
         for (int i = 0; i < variants.length; i++) {
             // loader==null if we're launched off the Java boot classpath
 final InputStream input = loader==null ? ClassLoader.getSystemResourceAsStream(variants[i]) : loader.getResourceAsStream(variants[i]);
             if (input == null)
                 continue;
             try {
                 final MessagesProperties properties = new MessagesProperties(fields, bundleName, isAccessible);
                 properties.load(input);
             } catch (IOException e) {
                 log(SEVERITY_ERROR, "Error loading " + variants[i], e); //$NON-NLS-1$
 } finally {
                 if (input != null)
                     try {
                         input.close();
                     } catch (IOException e) {
                         // ignore
 }
             }
         }
         computeMissingMessages(bundleName, clazz, fields, fieldArray, isAccessible);
         if (Debug.DEBUG_MESSAGE_BUNDLES)
             System.out.println("Time to load message bundle: " + bundleName + " was " + (System.currentTimeMillis() - start) + "ms."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 }
     
     /*
      * The method adds a log entry based on the error message and exception.
      * The output is written to the System.err.
      *
      * This method is only expected to be called if there is a problem in
      * the NLS mechanism. As a result, translation facility is not available
      * here and messages coming out of this log are generally not translated.
      *
      * @param severity - severity of the message (SEVERITY_ERROR or SEVERITY_WARNING)
      * @param message - message to log
      * @param e - exception to log
      */
     static void log(int severity, String message, Exception e) {
         if (frameworkLog != null) {
             frameworkLog.log(new FrameworkLogEntry("org.eclipse.osgi", severity, 1, message, 0, e, null)); //$NON-NLS-1$
 return;
         }
         String statusMsg;
         switch (severity) {
             case SEVERITY_ERROR :
                 statusMsg = "Error: "; //$NON-NLS-1$
 break;
             case SEVERITY_WARNING :
             // intentionally fall through:
 default :
                 statusMsg = "Warning: "; //$NON-NLS-1$
 }
         if (message != null)
             statusMsg += message;
         if (e != null)
             statusMsg += ": " + e.getMessage(); //$NON-NLS-1$
 System.err.println(statusMsg);
         if (e != null)
             e.printStackTrace();
     }

     /*
      * Class which sub-classes java.util.Properties and uses the #put method
      * to set field values rather than storing the values in the table.
      */
     private static class MessagesProperties extends Properties {

         private static final int MOD_EXPECTED = Modifier.PUBLIC | Modifier.STATIC;
         private static final int MOD_MASK = MOD_EXPECTED | Modifier.FINAL;
         private static final long serialVersionUID = 1L;

         private final String bundleName;
         private final Map fields;
         private final boolean isAccessible;

         public MessagesProperties(Map fieldMap, String bundleName, boolean isAccessible) {
             super();
             this.fields = fieldMap;
             this.bundleName = bundleName;
             this.isAccessible = isAccessible;
         }

         /* (non-Javadoc)
          * @see java.util.Hashtable#put(java.lang.Object, java.lang.Object)
          */
         public synchronized Object put(Object key, Object value) {
             Object fieldObject = fields.put(key, ASSIGNED);
             // if already assigned, there is nothing to do
 if (fieldObject == ASSIGNED)
                 return null;
             if (fieldObject == null) {
                 final String msg = "NLS unused message: " + key + " in: " + bundleName;//$NON-NLS-1$ //$NON-NLS-2$
 if (Debug.DEBUG_MESSAGE_BUNDLES)
                     System.out.println(msg);
                 log(SEVERITY_WARNING, msg, null);
                 return null;
             }
             final Field field = (Field ) fieldObject;
             //can only set value of public static non-final fields
 if ((field.getModifiers() & MOD_MASK) != MOD_EXPECTED)
                 return null;
             try {
                 // Check to see if we are allowed to modify the field. If we aren't (for instance
 // if the class is not public) then change the accessible attribute of the field
 // before trying to set the value.
 if (!isAccessible)
                     field.setAccessible(true);
                 // Set the value into the field. We should never get an exception here because
 // we know we have a public static non-final field. If we do get an exception, silently
 // log it and continue. This means that the field will (most likely) be un-initialized and
 // will fail later in the code and if so then we will see both the NPE and this error.
 field.set(null, value);
             } catch (Exception e) {
                 log(SEVERITY_ERROR, "Exception setting field value.", e); //$NON-NLS-1$
 }
             return null;
         }
     }
 }
